package iit.projects.etaxi.model.service;

import iit.projects.etaxi.model.dao.DriverDao;
import iit.projects.etaxi.model.dao.ReservationDao;
import iit.projects.etaxi.model.dao.SmsDao;
import iit.projects.etaxi.model.dao.VehicleDao;
import iit.projects.etaxi.model.entity.Driver;
import iit.projects.etaxi.model.entity.ReceivedSms;
import iit.projects.etaxi.model.entity.Reservation;
import iit.projects.etaxi.model.entity.SendingSms;
import iit.projects.etaxi.model.entity.Vehicle;
import iit.projects.etaxi.model.entity.VehicleLocation;
import iit.projects.etaxi.model.exception.DriverNotFoundException;
import iit.projects.etaxi.model.exception.ReservationNotFoundException;
import iit.projects.etaxi.util.Constants;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Transactional(readOnly=true)
public class SmsServiceImpl implements SmsService {
	
	final Logger log = LoggerFactory.getLogger(SmsServiceImpl.class);

	private SmsDao smsDao;
	private ReservationDao reservationDao;
	private DriverDao driverDao;
	private VehicleDao vehicleDao;
	
	private float replyProcessingDelay;
	
	@Override
	@Transactional(readOnly=false, propagation=Propagation.REQUIRES_NEW)
	public void processSms() {
		
		//Find New Received SMS
		List<ReceivedSms> receivedSms = smsDao.findReceivedSmsByStatus(Constants.SmsStatus.NEW);
		
		if(receivedSms != null && !receivedSms.isEmpty()){
			
			List<ReceivedSms> completionSms = new ArrayList<ReceivedSms>();
			List<ReceivedSms> acceptanceSms = new ArrayList<ReceivedSms>();
			
			//Categorize the SMS into two types
			for(ReceivedSms sms : receivedSms){
				
				String message = sms.getMessage();
				String[] msgContents = message.split(" ");
				
				if(msgContents != null){
					if(msgContents.length == 4){
						acceptanceSms.add(sms);
					}
					else if(msgContents.length == 3){
						completionSms.add(sms);
					}
				}
			}
			
			processJobAcceptanceSms(acceptanceSms);
			processJobCompletionSms(completionSms);
		}
	}
	
	private void processJobAcceptanceSms(List<ReceivedSms> sms){
		//Nothing to process
		if(sms == null || sms.isEmpty()){
			return;
		}
		
		//Group Received sms by Reservation.
		Map<Reservation, List<ReceivedSms>> map = groupByReservation(sms);
		if(map != null && !map.isEmpty()){
			
			Calendar currentTime = Calendar.getInstance();
			Map<Reservation, List<ReceivedSms>> processingMap = new HashMap<Reservation, List<ReceivedSms>>();
			
			//Compare current time with the last processed time of Reservations. 
			for(Reservation reservation : map.keySet()){
				Calendar reservationTime = new GregorianCalendar();
				reservationTime.setTime(reservation.getLastProcessedTime());
				
				long diffInMins = (currentTime.getTimeInMillis() - reservationTime.getTimeInMillis())/(60*1000);
				//If difference is greater than the replyProcessingDelay, add to processing list.
				if(diffInMins >= replyProcessingDelay){
					processingMap.put(reservation, map.get(reservation));
				}
			}
			
			if(processingMap != null && !processingMap.isEmpty()){
				for(Reservation reservation : processingMap.keySet()){
					//Check whether the reservation is already assigned.
					if(!reservation.getStatus().equals(Constants.ReservationStatus.PROCESSED)){
						continue;
					}
					List<ReceivedSms> smsList = processingMap.get(reservation);
					
					ReceivedSms selected = findSmsWithMinimumPickupDuration(smsList);
					try {
						Driver assignedDriver = driverDao.findDriverByContactNumber(selected.getSender());
						
						//Update Vehicle Details
						Vehicle vehicle = assignedDriver.getVehicle();
						vehicle.setOnHire(true);
						for(VehicleLocation location : vehicle.getVehicleLocations()){
							location.setStatus(Constants.VehicleLocationStatus.ASSIGNED_NEW);
						}
						
						Calendar calendar = Calendar.getInstance();
						calendar.setTime(reservation.getPickUpTime());
						calendar.add(Calendar.MINUTE, Integer.parseInt(selected.getMessage().split(" ")[3]));
						
						VehicleLocation vehicleLocation = new VehicleLocation();
						vehicleLocation.setLocation(reservation.getToSubCity());
						vehicleLocation.setReservation(reservation);
						vehicleLocation.setTime(calendar.getTime());
						vehicleLocation.setStatus(Constants.VehicleLocationStatus.LAST_ASSIGNED);
						vehicleLocation.setVehicle(vehicle);
						
						vehicle.getVehicleLocations().add(vehicleLocation);
						vehicleDao.updateVehicle(vehicle);
						
						//Update Reservation Details
						SendingSms sendingSms = new SendingSms();
						sendingSms.setMessage(reservation.getCode() + " Confirmed " + " | " +
								reservation.getCustomerName() + " | " + reservation.getCustomerContact());
						sendingSms.setRecepient(assignedDriver.getContactNumber());
						sendingSms.setReservation(reservation);
						sendingSms.setType(Constants.SmsTypes.CONFIRMATION);
						
						reservation.setLastProcessedTime(new Date());
						reservation.setStatus(Constants.ReservationStatus.ASSIGNED);
						reservation.setDriver(assignedDriver);
						reservation.getSms().add(sendingSms);
						reservationDao.updateReservation(reservation);
						
					} catch (DriverNotFoundException e) {
						log.error("ERROR : No Driver found for contact number " + selected.getSender());
					} catch(NumberFormatException e){
						log.error("ERROR : value is not numeric " + selected.getMessage().split(" ")[3]);
					}
					
					smsDao.updateSmsToProcessed(smsList);
				}
			}
		}
	}
	
	private Map<Reservation, List<ReceivedSms>> groupByReservation(List<ReceivedSms> sms){
		
		Set<String> reservationCodes = new HashSet<String>();
		Map<Reservation, List<ReceivedSms>> reservations = new HashMap<Reservation, List<ReceivedSms>>();
		
		for(ReceivedSms receivedSms : sms){
			
			String[] msgContent = receivedSms.getMessage().split(" ");
			String reservationCode = msgContent[0];
			
			if(reservationCodes.add(reservationCode)){
				try {
					Reservation reservation = reservationDao.findByCode(reservationCode);
					List<ReceivedSms> smsList = new ArrayList<ReceivedSms>();
					smsList.add(receivedSms);
					
					reservations.put(reservation, smsList);
					
				} catch (ReservationNotFoundException e) {
					log.error("ERROR : No Reservation found for code " + reservationCode);
				}
			}
			else{
				reservations.get(getReservationByCode(reservations.keySet(), reservationCode)).
									add(receivedSms);
			}
		}
		return reservations;
	}
	
	private Reservation getReservationByCode(Set<Reservation> reservations, String code){
		for(Reservation reservation : reservations){
			if(reservation.getCode().equals(code)){
				return reservation;
			}
		}
		return null;
	}
	
	private ReceivedSms findSmsWithMinimumPickupDuration(List<ReceivedSms> smsList){
		int minimumPickupDuration = 0;
		ReceivedSms selected = null;
		
		for(ReceivedSms receivedSms : smsList){
			String[] messageContent = receivedSms.getMessage().split(" ");
			try{
				int pickUpDuration = Integer.parseInt(messageContent[2]);
				if(minimumPickupDuration == 0){
					minimumPickupDuration = pickUpDuration;
					selected = receivedSms;
				}
				else{
					if(minimumPickupDuration > pickUpDuration){
						minimumPickupDuration = pickUpDuration;
						selected = receivedSms;
					}
				}
			}catch(NumberFormatException e){
				log.error("Value is not numeric " + messageContent[2]);
			}
		}
		return selected; 
	}
	
	private void processJobCompletionSms(List<ReceivedSms> sms){
		//Nothing to Process
		if(sms == null || sms.isEmpty()){
			return;
		}
		Calendar calendar = Calendar.getInstance();
		for(ReceivedSms receivedSms : sms){
			String reservationCode = receivedSms.getMessage().split(" ")[0];
			try {
				Reservation reservation = reservationDao.findByCode(reservationCode);
				
				reservation.setLastProcessedTime(calendar.getTime());
				reservation.setStatus(Constants.ReservationStatus.COMPLETED);
				double charge = Double.parseDouble(receivedSms.getMessage().split(" ")[2]);
				reservation.setCharge(charge);
				reservationDao.updateReservation(reservation);
				receivedSms.setStatus(Constants.SmsStatus.PROCESSED);
				
			} catch (ReservationNotFoundException e) {
				log.error("No Reservation found for Code " + reservationCode);
			} catch(NumberFormatException e){
				log.error("Non Numeric value encountered where a numeric value was expected " + e);
			}
		}
		smsDao.updateSmsToProcessed(sms);
	}
	
	public void setSmsDao(SmsDao smsDao) {
		this.smsDao = smsDao;
	}

	public void setReservationDao(ReservationDao reservationDao) {
		this.reservationDao = reservationDao;
	}

	public void setDriverDao(DriverDao driverDao) {
		this.driverDao = driverDao;
	}

	public void setVehicleDao(VehicleDao vehicleDao) {
		this.vehicleDao = vehicleDao;
	}

	public void setReplyProcessingDelay(float replyProcessingDelay) {
		this.replyProcessingDelay = replyProcessingDelay;
	}

}
